package tool

import (
	"MoSkeleton/config/consts"
	"MoSkeleton/framework"
	consts2 "MoSkeleton/framework/core/consts"
	"MoSkeleton/framework/core/dto/response"
	"MoSkeleton/framework/core/moerrors"
	"MoSkeleton/framework/utils"
	tool3 "MoSkeleton/models/tool"
	"MoSkeleton/services/base"
	"MoSkeleton/services/tool/logic"
	"MoSkeleton/web/pogos/request/tool"
	tool2 "MoSkeleton/web/pogos/response/tool"
	"archive/zip"
	"bytes"
	"fmt"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"
	"gorm.io/gorm"
	"html/template"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

type saveImportTableTemplateCallback func(genTables []*tool3.GenTable, datasource *tool3.GenDatasource) error
type CodeGenService struct {
	base.BaseService
}

func (g *CodeGenService) GetGenTablePage(pageReq *tool.CodeGenPageReq) (*response.PageResult, error) {
	db := g.getGenTableVo(g.GetDb(nil))
	result := make([]*tool2.CodeGenItemResp, 0)
	var total int64
	if err := db.Count(&total).Offset(pageReq.Offset()).Limit(pageReq.Limit()).Find(&result).Error; err != nil {
		return nil, err
	}
	pageResult := response.NewFromPageInfoAndData(&pageReq.PageInfo, &result, total)
	return pageResult, nil
}

func (g *CodeGenService) SaveImportTable(importTableReq *tool.ImportTableReq, currentUsername string) error {
	return g.saveImportTableTemplate(importTableReq, currentUsername, func(genTables []*tool3.GenTable, datasource *tool3.GenDatasource) error {
		err := base.BaseDb.Transaction(func(tx *gorm.DB) error {
			if _, err := genTableService.SaveTableByImport(tx, genTables); err != nil {
				return err
			}
			_, err := genTableColumnService.SaveColumnsByGenTables(tx, datasource.DatasourceType, genTables, currentUsername)
			if err != nil {
				return err
			}
			return nil
		})
		return err
	})
}

func (g *CodeGenService) SyncDb(importTableReq *tool.ImportTableReq, currentUsername string) error {
	return g.saveImportTableTemplate(importTableReq, currentUsername, func(genTables []*tool3.GenTable, datasource *tool3.GenDatasource) error {
		oldGenTables, err := genTableService.GetGetTablesByTableNames(nil, strings.Split(importTableReq.Tables, ","))
		if err != nil {
			return err
		}
		for _, oldGenTable := range oldGenTables {
			genTable := g.findByTableName(genTables, oldGenTable.SourceTableName)
			genTable.ID = oldGenTable.ID
			genTable.CreatedAt = nil
			genTable.CreatedBy = ""
		}

		err = base.BaseDb.Transaction(func(tx *gorm.DB) error {
			if _, err := genTableService.SaveTableByImport(tx, genTables); err != nil {
				return err
			}
			if _, err := genTableColumnService.SyncColumnsByGenTables(tx, datasource.DatasourceType, genTables[0], currentUsername); err != nil {
				return err
			}
			return nil
		})
		return err
	})
}
func (g *CodeGenService) findByTableName(genTables []*tool3.GenTable, tableName string) *tool3.GenTable {
	for _, genTable := range genTables {
		if genTable.SourceTableName == tableName {
			return genTable
		}
	}
	return nil
}

func (g *CodeGenService) GetDbTableList(bizDb *gorm.DB, pageReq *tool.DbTablePageReq) (*response.PageResult, error) {
	db, err := g.getDbTableVo(g.GetDb(bizDb), pageReq)
	if err != nil {
		return nil, err
	}
	itemsResp := make([]*tool2.DbTableItemResp, 0)
	var total int64
	if err := db.Count(&total).Offset(pageReq.Offset()).Limit(pageReq.Limit()).Find(&itemsResp).Error; err != nil {
		return nil, err
	}
	pageResult := response.NewFromPageInfoAndData(&pageReq.PageInfo, &itemsResp, total)
	return pageResult, nil
}

func (g *CodeGenService) DeleteCodeGenByIds(ids *[]uint64) error {
	err := base.BaseDb.Transaction(func(tx *gorm.DB) error {
		if _, err := genTableService.DeleteTableByIds(tx, ids); err != nil {
			return err
		}
		_, err := genTableColumnService.DeleteColumnsByTableIds(tx, ids)
		return err
	})
	return err
}

// TODO
func (g *CodeGenService) DownloadByTable(tableName string) (string, error) {
	g.generateCode(tableName)
	return "", nil
}

func (g *CodeGenService) GetCodeGenDetail(bizId, tableId uint64) (*tool2.CodeGenDetailResp, error) {
	var err error
	codeGenDetailResp := new(tool2.CodeGenDetailResp)
	codeGenDetailResp.Info, err = genTableService.GetDetailById(tableId)
	if err != nil {
		return nil, err
	}
	codeGenDetailResp.Tables, err = genTableService.GetDetailItemsByBizId(bizId)
	return codeGenDetailResp, nil
}

func (g *CodeGenService) GetGormDbByBizId(bizId uint64) (*gorm.DB, error) {
	datasource, err := genBizService.GetDatasourceByBizId(bizId)
	if err != nil {
		return nil, err
	}
	if datasource.DbPasswd != "" {
		datasource.DbPasswd, err = utils.Sm4Decrypt(utils.Store_Sm4Key, datasource.DbPasswd)
		if err != nil {
			return nil, err
		}
	}

	datasourcePogo := new(logic.DatasourcePogo)
	err = utils.CopyStruct(datasourcePogo, datasource)
	if err != nil {
		return nil, err
	}
	return logic.NewGenDb(datasourcePogo)
}

func (g *CodeGenService) UpdateCodeGen(updateReq *tool.CodeGenUpdateReq) error {
	return genTableService.UpdateCodeGenTable(&updateReq.GenTableUpdateReq)
}

func (g *CodeGenService) PreviewGen(tableId uint64) map[string]string {
	//TODO
	return nil
}

func (g *CodeGenService) BatchGenCode(bizId uint64, tableIds *[]uint64) (*bytes.Buffer, error) {
	classifyItem, err := genTemplateClassifyService.GetClassifyByBizId(bizId)
	if err != nil {
		return nil, err
	}
	codeGeneratorResps, err := g.getCodeGeneratorsByTableIds(tableIds)
	if err != nil {
		return nil, err
	}
	waitGroup := new(sync.WaitGroup)
	tplChannel := make(chan *logic.TplFileBuffer, len(consts.TplFilesByTable)*len(*tableIds))
	errChannel := make(chan error, 1)
	g.genCodeAsAggregate(codeGeneratorResps, tplChannel, errChannel, classifyItem, waitGroup)
	g.genCodeAsModuleAggregate(codeGeneratorResps, tplChannel, errChannel, classifyItem, waitGroup)
	g.genCodeAsTables(codeGeneratorResps, tplChannel, errChannel, classifyItem, waitGroup)
	zipBuffer, err := g.zipTplFileBuffers(tplChannel, errChannel, waitGroup)
	if err != nil {
		return nil, err
	}
	return zipBuffer, nil
}

func (g *CodeGenService) genCodeAsModuleAggregate(codeGeneratorResps []*tool2.CodeGeneratorResp, tplChannel chan<- *logic.TplFileBuffer,
	errChannel chan<- error, classifyItem *tool2.GenTemplateClassifyItemResp, waitGroup *sync.WaitGroup) {
	itemResps := tool2.NewCodeGeneratorModuleAggregate(codeGeneratorResps)
	for _, itemResp := range itemResps {
		waitGroup.Add(1)
		go func(itemResp *tool2.CodeGeneratorModuleAggregateItemResp) {
			defer func() { waitGroup.Done() }()
			err := logic.GenCodeAsModuleAggregate(classifyItem.ClassifyCode, itemResp, tplChannel)
			if err != nil {
				errChannel <- err
				return
			}
		}(itemResp)
	}
}

func (g *CodeGenService) genCodeAsAggregate(codeGeneratorResps []*tool2.CodeGeneratorResp, tplChannel chan<- *logic.TplFileBuffer,
	errChannel chan<- error, classifyItem *tool2.GenTemplateClassifyItemResp, waitGroup *sync.WaitGroup) {
	waitGroup.Add(1)
	go func() {
		codeGeneratorAggregateResp := tool2.NewCodeGeneratorAggregate(codeGeneratorResps)
		err := logic.GenCodeAsAggregate(classifyItem.ClassifyCode, codeGeneratorAggregateResp, tplChannel)
		waitGroup.Done()
		if err != nil {
			errChannel <- err
			return
		}
	}()
}
func (g *CodeGenService) genCodeAsTables(codeGeneratorResps []*tool2.CodeGeneratorResp, tplChannel chan<- *logic.TplFileBuffer,
	errChannel chan<- error, classifyItem *tool2.GenTemplateClassifyItemResp, waitGroup *sync.WaitGroup) {
	for _, codeGeneratorResp := range codeGeneratorResps {
		waitGroup.Add(1)
		go func(codeGeneratorResp *tool2.CodeGeneratorResp) {
			codeGeneratorResp.IgnoreTablePrefix = classifyItem.IsIgnoreTablePrefix == consts.U_YES
			codeGeneratorResp.PermissionPrefix = g.genPermissionPrefix(codeGeneratorResp.TableDetailResp)
			err := logic.GenCodeAsTable(classifyItem.ClassifyCode, codeGeneratorResp, tplChannel)
			waitGroup.Done()
			if err != nil {
				errChannel <- err
				return
			}
		}(codeGeneratorResp)
	}
}

func (g *CodeGenService) ValidatorCodeGenUpdate(updateReq *tool.CodeGenUpdateReq) error {
	if updateReq.TplCategory == consts.TPL_TREE {
		var ok bool
		ok = utils.MapKeyExistsAndValueIsString(updateReq.Params, consts.TREE_CODE)
		if !ok {
			return moerrors.NewValidateError(consts2.ValidatorParamsCheckFailCode, "树编码字段不能为空")
		}
		ok = utils.MapKeyExistsAndValueIsString(updateReq.Params, consts.TREE_PARENT_CODE)
		if !ok {
			return moerrors.NewValidateError(consts2.ValidatorParamsCheckFailCode, "树父编码字段不能为空")
		}
		ok = utils.MapKeyExistsAndValueIsString(updateReq.Params, consts.TREE_NAME)
		if !ok {
			return moerrors.NewValidateError(consts2.ValidatorParamsCheckFailCode, "树名称字段不能为空")
		}
		if updateReq.TplCategory == consts.TPL_SUB {
			if updateReq.SubTableName == "" {
				return moerrors.NewValidateError(consts2.ValidatorParamsCheckFailCode, "关联子表的表名不能为空")
			}
			if updateReq.SubTableFkName == "" {
				return moerrors.NewValidateError(consts2.ValidatorParamsCheckFailCode, "子表关联的外键不能为空")
			}
		}
	}
	return nil
}

func (g *CodeGenService) zipTplFileBuffers(tplChannel chan *logic.TplFileBuffer,
	errChannel chan error, waitGroup *sync.WaitGroup) (*bytes.Buffer, error) {
	var buffer bytes.Buffer
	zipWriter := zip.NewWriter(&buffer)
	defer func() {
		_ = zipWriter.Close()
	}()
	go func() {
		waitGroup.Wait()
		close(tplChannel)
		//如果将errChannel关闭会导致select中有可能err先收到消息，从而导致返回错误的数据
		//close(errChannel)
	}()
	for {
		var ok bool
		var tplFileBuffer *logic.TplFileBuffer
		select {
		case tplFileBuffer, ok = <-tplChannel:
			if !ok || tplFileBuffer == nil {
				break
			}
			writer, err := zipWriter.Create(tplFileBuffer.GetOutFilePathName())
			if err != nil {
				return nil, err
			}
			_, err = io.Copy(writer, tplFileBuffer.Buffer)
			if err != nil {
				return nil, err
			}
		case err := <-errChannel:
			return nil, err
		}
		if !ok {
			break
		}
	}
	return &buffer, nil
}
func (g *CodeGenService) genTableFromDbTableItems(tableItemsResp *[]*tool2.DbTableItemResp, genBiz *tool3.GenBiz, currentUsernname string) ([]*tool3.GenTable, error) {
	genTables := make([]*tool3.GenTable, 0)
	var err error
	for _, tableItem := range *tableItemsResp {
		genTable := &tool3.GenTable{}
		if err = utils.CopyStruct(genTable, tableItem); err != nil {
			return nil, err
		}
		genTable.SourceTableName = tableItem.TableName
		genTable.CreatedBy = currentUsernname
		genTable.UpdatedBy = currentUsernname
		genTable.BizId = genBiz.ID
		genTable.FunctionAuthor = genBiz.FunctionAuthor
		err = genTableService.CopyFieldsByTablePrefix(genBiz.ID, genBiz.TableSuffix, genTable)
		if err != nil {
			return nil, err
		}
		logic.InitGenTableForNew(genTable, genBiz)
		genTables = append(genTables, genTable)
	}
	return genTables, nil
}

func (g *CodeGenService) genPermissionPrefix(tableDetailResp *tool2.GenTableDetailResp) string {
	return fmt.Sprintf("%s:%s", tableDetailResp.ModuleName, logic.FirstLetterLower(tableDetailResp.StructName))
}

func (g *CodeGenService) getDbTableListByNames(tableNames []string) (*[]*tool2.DbTableItemResp, error) {
	db := g.GetDb(nil).Table("information_schema.tables").Select("table_name, table_comment, create_time, update_time")
	db.Where("table_name NOT LIKE 'qrtz\\_%' and table_name NOT LIKE 'gen\\_%' and table_schema = (select database())")
	db.Where("table_name IN ?", tableNames)
	itemsResp := make([]*tool2.DbTableItemResp, 0)
	if err := db.Find(&itemsResp).Error; err != nil {
		return nil, err
	}
	return &itemsResp, nil
}
func (g *CodeGenService) generateCode(tableName string) error {
	columns, err := getColumns(g.GetDb(nil), tableName)
	if err != nil {
		return err
	}
	tc := cases.Title(language.Und)
	data := map[string]interface{}{
		"TableName":            tc.String(tableName),
		"Columns":              columns,
		"ColumnNames":          getColumnNames(columns),
		"ColumnNamesWithoutId": getColumnNamesWithoutId(columns),
	}

	templates := []struct {
		TemplatePath string
		OutputPath   string
	}{
		{framework.WorkDir + "assets/codegen/default/model.tpl", filepath.Join("generated", "models", tableName+".go")},
	}

	for _, t := range templates {
		tmpl, err := template.ParseFiles(t.TemplatePath)
		if err != nil {
			return err
		}

		file, err := os.Create(t.OutputPath)
		if err != nil {
			return err
		}
		defer file.Close()

		if err := tmpl.Execute(file, data); err != nil {
			return err
		}
	}

	return nil
}
func getColumns(db *gorm.DB, tableName string) ([]Column, error) {
	sqlDb, _ := db.DB()
	rows, err := sqlDb.Query(fmt.Sprintf("SHOW COLUMNS FROM %s", tableName))
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var columns []Column
	for rows.Next() {
		var col Column
		if err := rows.Scan(&col.Name, &col.Type, &col.Null, &col.Key, &col.Default, &col.Extra); err != nil {
			return nil, err
		}
		columns = append(columns, col)
	}
	return columns, nil
}

func getColumnNames(columns []Column) []string {
	names := make([]string, len(columns))
	for i, col := range columns {
		names[i] = col.Name
	}
	return names
}

func getColumnNamesWithoutId(columns []Column) []string {
	names := make([]string, 0)
	for _, col := range columns {
		if col.Name != "id" {
			names = append(names, col.Name)
		}
	}
	return names
}

type Column struct {
	Name    string
	Type    string
	Null    string
	Key     string
	Default string
	Extra   string
}

func (g *CodeGenService) getGenTableVo(db *gorm.DB) *gorm.DB {
	selectFields := "id, source_table_name, table_comment, sub_table_id, sub_table_fk_name, struct_name, tpl_category, tpl_web_type, package_name, module_name, business_name, function_name, function_author, gen_type, gen_path, options, created_by, created_at, updated_by, updated_at, remark"
	return db.Table("gen_table").Select(selectFields)
}

func (g *CodeGenService) importGenTable(itemsResp *tool2.DbTableItemResp, currentUsername string) {

}

func (g *CodeGenService) getDbTableVo(bizDb *gorm.DB, pageReq *tool.DbTablePageReq) (*gorm.DB, error) {
	tableSimpleItems, err := genTableService.GetAllTableByBizId(pageReq.BizId)
	if err != nil {
		return nil, err
	}
	tableNames := utils.FieldsFromSlice(tableSimpleItems, "SourceTableName")
	selectFields := "table_name, table_comment, engine, create_time, update_time"
	bizDb = bizDb.Table("information_schema.tables").Select(selectFields)
	bizDb.Where("table_schema = (select database())")
	bizDb.Where("table_name NOT LIKE 'qrtz\\_%' AND table_name NOT LIKE 'gen\\_%'")
	if len(tableNames) > 0 {
		bizDb.Where("table_name NOT IN (?)", tableNames)
	}

	if pageReq.TableName != "" {
		bizDb.Where("table_name like concat('%', ?, '%')", pageReq.TableName)
	}
	if pageReq.TableComment != "" {
		bizDb.Where("table_comment like concat('%', ?, '%')", pageReq.TableComment)
	}
	beginTime, ok := pageReq.GetParamValue("beginTime")
	if ok && beginTime != "" {
		if beginTime = pageReq.FormatBeginTime(beginTime); beginTime != "" {
			bizDb.Where("create_time >= ?", beginTime)
		}
	}
	endTime, ok := pageReq.GetParamValue("endTime")
	if ok && endTime != "" {
		if endTime = pageReq.FormatEndTime(endTime); endTime != "" {
			bizDb.Where("create_time <= ?", endTime)
		}
	}
	bizDb.Order("create_time desc")
	return bizDb, nil
}

func (g *CodeGenService) getCodeGeneratorsByTableIds(tableIds *[]uint64) ([]*tool2.CodeGeneratorResp, error) {
	codeGeneratorResps := make([]*tool2.CodeGeneratorResp, 0, len(*tableIds))
	for _, tableId := range *tableIds {
		codeGeneratorResp, err := genTableService.GetCodeGeneratorById(tableId)
		if err != nil {
			return nil, moerrors.NewValidateMsgError(consts2.ValidatorParamsCheckFailCode, "生成代码失败", err)
		}
		codeGeneratorResps = append(codeGeneratorResps, codeGeneratorResp)
	}
	return codeGeneratorResps, nil
}

func (g *CodeGenService) saveImportTableTemplate(importTableReq *tool.ImportTableReq, currentUsername string, callback saveImportTableTemplateCallback) error {
	tableItemsResp, err := g.getDbTableListByNames(strings.Split(importTableReq.Tables, ","))
	if err != nil {
		return err
	}
	genBiz := new(tool3.GenBiz)
	err = genBiz.GetById(nil, importTableReq.BizId, genBiz)
	if err != nil {
		return err
	}
	datasource, err := genBizService.GetDatasourceByBizId(importTableReq.BizId)
	if err != nil {
		return err
	}
	if genTables, err := g.genTableFromDbTableItems(tableItemsResp, genBiz, currentUsername); err != nil {
		return err
	} else {
		return callback(genTables, datasource)
	}
}
